﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Globalization;
using System.IO;
using System.Threading;
using System.Windows.Forms;

namespace NExplorer.Controls
{
	internal sealed class ExplorerListView : ListView
	{
		#region ColumnWorkItem class

		private class ColumnWorkItem
		{
			public Guid					DirectoryId;
			public FileSystemInfo		FileSystemInfo;
			public ColumnProviderInfo	ColumnProviderInfo;

			public ColumnWorkItem(ColumnProviderInfo columnProviderInfo, Guid directoryId, FileSystemInfo fileSystemInfo)
			{
				ColumnProviderInfo = columnProviderInfo;
				DirectoryId = directoryId;
				FileSystemInfo = fileSystemInfo;
			}
		}

		#endregion

		#region ExplorerListViewDefaultComparer

		private class ExplorerListViewComparer : IComparer<FileSystemListViewItem>, IComparer
		{
			private readonly IComparer	_comparer;
			private readonly int		_columnIndex;
			private readonly int		_ascendingMultiplier;

			public ExplorerListViewComparer(int columnIndex, IComparer comparer, bool ascending)
			{
				_columnIndex = columnIndex;
				_comparer = comparer;
				_ascendingMultiplier = ascending ? 1 : -1;
			}

			public int Compare(FileSystemListViewItem x, FileSystemListViewItem y)
			{
				int result;

				// Adjust for Explorer-like folder priority
				if (x.IsDirectory && !y.IsDirectory)
				{
					result = -1;
				}
				else if (!x.IsDirectory && y.IsDirectory)
				{
					result = 1;
				}
				else
				{
					ExplorerListViewSubItem subItemX = x.SubItems[_columnIndex] as ExplorerListViewSubItem;

					if (subItemX == null)
					{
						result = _comparer.Compare(x.SubItems[_columnIndex].Text, y.SubItems[_columnIndex].Text);
					}
					else
					{
						ExplorerListViewSubItem subItemY = (ExplorerListViewSubItem)y.SubItems[_columnIndex];
						result = _comparer.Compare(subItemX.ColumnItemData.Data, subItemY.ColumnItemData.Data);
					}
				}

				return result * _ascendingMultiplier;
			}

		    public int Compare(object x, object y)
		    {
		        return Compare((FileSystemListViewItem)x, (FileSystemListViewItem)y);
		    }
		}

		#endregion

		public event EventHandler				ItemCountChanged;

		private readonly FileSystemWatcher		_fsWatcher = new FileSystemWatcher();
		private readonly ImageList				_smallListViewImageList;
		private readonly ExplorerService		_explorerService;
		private readonly ColumnProviderManager	_columnProviderManager;
		private readonly Dictionary<string, FileSystemListViewItem>	
												_listViewItems = new Dictionary<string, FileSystemListViewItem>();
		private readonly List<IComparer>		_columnComparers = new List<IComparer>();
		private Guid							_currentDirectoryId;
		private int								_lastColumnSorted;
		private bool							_lastColumnSortedAscending = true;

        public ExplorerListView()
		{
            DoubleBuffered = true;

			_explorerService = (ExplorerService)ServiceManager.Explorer;
			_columnProviderManager = (ColumnProviderManager)ServiceManager.ColumnProviderManager;

			SmallImageList = _smallListViewImageList = new ImageList()
			{
				ImageSize = new Size(16, 16),
				ColorDepth = ColorDepth.Depth32Bit
			};

			_fsWatcher.IncludeSubdirectories = false;
			_fsWatcher.Changed += HandleFileSystemChange;
			_fsWatcher.Deleted += HandleFileSystemChange;
			_fsWatcher.Renamed += HandleFileSystemChange;
			_fsWatcher.Created += HandleFileSystemChange;
		}

		protected override void InitLayout()
		{
			base.InitLayout();

			if (!DesignMode)
			{
				_explorerService.CurrentDirectoryChanged += delegate { Populate(); };
				_explorerService.ColumnItemUpdated += ColumnItemUpdated;
				_explorerService.RefreshRequested += delegate { Populate(); };

				_columnProviderManager.ColumnProviderAdded += WhenColumnProviderAdded;
				_columnProviderManager.ColumnProviderRemoved += WhenColumnProviderRemoved;

				_explorerService.SelectedItemsChanged += WhenSelectedItemsChanged;

				// Create columns
				Reset();
			}
		}

		protected override void OnHandleCreated(EventArgs e)
        {
            base.OnHandleCreated(e);
            Utilities.SetExplorerStyle(this);
        }

	    private void WhenSelectedItemsChanged(object sender, EventArgs e)
	    {
			if (InvokeRequired)
			{
				Invoke(new EventHandler(WhenSelectedItemsChanged), sender, e);
			}
	    	else
	    	{
	    		StopWatchingForSelectionChanges();
	    		BeginUpdate();
            
	    		SelectedItems.Clear();

	    		foreach (FileSystemInfo fsi in ServiceManager.Explorer.SelectedItems)
	    		{
	    			FileSystemListViewItem lvi;
                
	    			if (_listViewItems.TryGetValue(fsi.FullName, out lvi))
	    			{
	    				lvi.Selected = true;   
	    			}
	    		}

	    		EndUpdate();
	    		StartWatchingForSelectionChanges();
	    	}
        }

	    private void StartWatchingForSelectionChanges()
	    {
	        SelectedIndexChanged += OnSelectedIndexChanged;
	    }

	    private void StopWatchingForSelectionChanges()
	    {
            SelectedIndexChanged -= OnSelectedIndexChanged;
        }

	    private void ColumnItemUpdated(Guid directoryId, int columnId, FileSystemInfo fileSystemInfo, ColumnItemData columnItemData)
		{
			// Only handle calls if the directory is the current directory.
			if (directoryId == _currentDirectoryId)
			{
				if (InvokeRequired)
				{
					Invoke(new ColumnItemUpdatedHandler(ColumnItemUpdated), directoryId, columnId, fileSystemInfo, columnItemData);
				}
				else
				{
					FileSystemListViewItem lvi = _listViewItems[fileSystemInfo.FullName];

					if (lvi != null)
					{
						while (lvi.SubItems.Count <= columnId)
						{
							lvi.SubItems.Add("");	
						}

						lvi.SubItems[columnId] = new ExplorerListViewSubItem(lvi, columnItemData);
					}
				}
			}
		}

	    private void WhenColumnProviderRemoved(object sender, EventArgs<IColumnProvider> e)
		{
			Reset();
		}

		private void WhenColumnProviderAdded(object sender, EventArgs<IColumnProvider> e)
		{
			Reset();
		}

		private void Reset()
		{
			Items.Clear();
			Columns.Clear();
			_columnComparers.Clear();

			Comparer comparer = new Comparer(CultureInfo.CurrentCulture);
			_columnComparers.Add(comparer); // Name
			_columnComparers.Add(comparer); // Date modified
			_columnComparers.Add(comparer); // Type
			_columnComparers.Add(comparer); // Size

			AddColumn("Name", 300);
			AddColumn("Date modified", 150);
			AddColumn("Type", 150);
			AddColumn("Size", 75, HorizontalAlignment.Right);

			foreach (ColumnProviderInfo providerInfo in _columnProviderManager.ProviderInfos)
			{
				IColumnProvider provider = providerInfo.ColumnProvider;
				_columnComparers.Add(provider.ItemComparer);
				AddColumn(provider.ColumnHeader, provider.DefaultWidth, provider.Alignment);
			}
		}

		private void AddColumn(string text, int width)
		{
			AddColumn(text, width, HorizontalAlignment.Left);
		}

		private void AddColumn(string text, int width, HorizontalAlignment alignment)
		{
			Columns.Add(new ColumnHeader() { Text = text, TextAlign = alignment, Width = width });
		}

		protected override void OnColumnClick(ColumnClickEventArgs e)
		{
			if (e.Column < Columns.Count)
			{
				bool ascending;

				if (_lastColumnSorted == e.Column)
				{
					ascending = !_lastColumnSortedAscending;
				}
				else
				{
					ascending = true;
				}

				ListViewItemSorter = new ExplorerListViewComparer(e.Column, _columnComparers[e.Column], ascending);

				_lastColumnSorted = e.Column;
				_lastColumnSortedAscending = ascending;
			}
		}

		protected override void OnMouseUp(MouseEventArgs e)
		{
			base.OnMouseUp(e);

			if (e.Button == MouseButtons.Right && _explorerService.CurrentDirectory != null)
			{
				ListViewItem lvi = GetItemAt(e.X, e.Y);

				if (lvi == null)
				{
					SelectedItems.Clear();
					_explorerService.SelectedItems = _explorerService.CurrentDirectory.ToArray();
				}

				ContextMenuStrip menu = _explorerService.CreateContextMenu();
				menu.Show(this, e.Location);
			}
		}

		protected override void OnKeyDown(KeyEventArgs e)
		{
			if (e.KeyCode == Keys.Enter)
			{
				e.Handled = true;
				NavigateToSelectedItem();
			}

			base.OnKeyDown(e);
		}

		protected override void OnMouseDoubleClick(MouseEventArgs e)
		{
			base.OnMouseDoubleClick(e);

			NavigateToSelectedItem();
		}

		private void NavigateToSelectedItem()
		{
			if (SelectedItems.Count == 1)
			{
				FileSystemListViewItem selectedItem = (FileSystemListViewItem)SelectedItems[0];
				_explorerService.NavigateTo(selectedItem.FileSystemInfo);
			}
		}

		protected void OnSelectedIndexChanged(object sender, EventArgs e)
		{
			List<FileSystemInfo> infos = new List<FileSystemInfo>();

			foreach (FileSystemListViewItem item in SelectedItems)
			{
				infos.Add(item.FileSystemInfo);
			}

            // If no items are selected, select the folder.
            if (infos.Count == 0)
            {
                infos.Add(_explorerService.CurrentDirectory);
            }

			_explorerService.SelectedItems = infos.ToArray();
		}

		private void HandleFileSystemChange(object sender, FileSystemEventArgs e)
		{
			if (InvokeRequired)
			{
				Invoke(new FileSystemEventHandler(HandleFileSystemChange), null, e);
			}
			else
			{
				switch (e.ChangeType)
				{
					case WatcherChangeTypes.Created:
						{
							AddItem(Utilities.CreateFileSystemInfo(e.FullPath));
							OnItemCountChanged();
							break;
						}
					case WatcherChangeTypes.Deleted:
						{
							RemoveItem(e.FullPath);
							break;
						}
					case WatcherChangeTypes.Changed:
						{
							if (File.Exists(e.FullPath) || Directory.Exists(e.FullPath))
							{
								PerformActionOnFoundItem(e.Name, delegate(FileSystemListViewItem item) { item.Refresh(); });
							}
							break;
						}
					case WatcherChangeTypes.Renamed:
						{
							string oldName = ((RenamedEventArgs)e).OldName;
							FileSystemInfo newFileSystemInfo;

							if (File.Exists(e.FullPath))
							{
								newFileSystemInfo = new FileInfo(e.FullPath);	
							}
							else if (Directory.Exists(e.FullPath))
							{
								newFileSystemInfo = new DirectoryInfo(e.FullPath);	
							}
							else
							{
								// Does exist anymore, so must've been renamed then moved (or deleted) by now.
								// We'll want to remove it from the display.
								newFileSystemInfo = null;
							}

							if (newFileSystemInfo == null)
							{
								RemoveItem(oldName);
							}
							else
							{
								FileSystemListViewItem item = GetItem(new LowerCaseString(oldName));

								if (item != null)
								{
									item.Rename(newFileSystemInfo);
									_listViewItems.Remove(oldName);
									_listViewItems.Add(newFileSystemInfo.FullName, item);
								}
							}
							break;
						}
				}
			}
		}

		private void RemoveItem(string name)
		{
			FileSystemListViewItem item = GetItem(new LowerCaseString(name));
			
			if (item != null)
			{
				item.Remove();
				_listViewItems.Remove(name);
				OnItemCountChanged();
			}
		}

		private void PerformActionOnFoundItem(string name, Action<FileSystemListViewItem> action)
		{
			FileSystemListViewItem item = GetItem(new LowerCaseString(name));
			
			if (item != null)
			{
				action(item);
			}					
		}

		private FileSystemListViewItem GetItem(LowerCaseString text)
		{
			FileSystemListViewItem foundItem = null;

			foreach (FileSystemListViewItem item in Items)
			{
				if (item.FileSystemInfo.FullName.ToLower() == text.Value)
				{
					foundItem = item;
					break;
				}
			}

			return foundItem;
		}

		private void Populate()
		{
			_currentDirectoryId = Guid.NewGuid();

			DirectoryInfo directory = ServiceManager.Explorer.CurrentDirectory;

			BeginUpdate();
			Items.Clear();
			_listViewItems.Clear();
			_smallListViewImageList.Images.Clear();

			try
			{
				Cursor = Cursors.WaitCursor;

				_fsWatcher.Path = directory.FullName;
				_fsWatcher.EnableRaisingEvents = true;

				foreach (FileSystemInfo fileSystemInfo in directory.GetFileSystemInfos())
				{
					AddItem(fileSystemInfo);
				}
			}
			catch (UnauthorizedAccessException)
			{
				string text = string.Format("Access to '{0}' is denied.", directory);
				MessageBox.Show(text, "Access Denied", MessageBoxButtons.OK, MessageBoxIcon.Warning);
			}
			catch (IOException e)
			{
				MessageBox.Show(e.Message, "IO Exception", MessageBoxButtons.OK, MessageBoxIcon.Warning);
			}
			finally
			{
				Cursor = Cursors.Default;
			}

			EndUpdate();

			OnItemCountChanged();
		}

		private void AddItem(FileSystemInfo fileSystemInfo)
		{
			if (Utilities.CanShowFileSystemItem(fileSystemInfo))
			{
				FileSystemListViewItem lvi = new FileSystemListViewItem(fileSystemInfo);
				lvi.Initialize(SmallImageList);
				Items.Add(lvi);

				_listViewItems.Add(fileSystemInfo.FullName, lvi);

				foreach (ColumnProviderInfo columnProviderInfo in _columnProviderManager.ProviderInfos)
				{
					if (columnProviderInfo.Enabled)
					{
						ThreadPool.QueueUserWorkItem(ProcessWorkItem, new ColumnWorkItem(columnProviderInfo, _currentDirectoryId, fileSystemInfo));
					}
				}
			}
		}

		private static void ProcessWorkItem(object state)
		{
			ColumnWorkItem workItem = (ColumnWorkItem)state;
			IColumnProvider columnProvider = workItem.ColumnProviderInfo.ColumnProvider;
		
			if (workItem.ColumnProviderInfo.Enabled)
			{
				try
				{
					columnProvider.ProcessItem(workItem.DirectoryId, workItem.ColumnProviderInfo.ColumnIndex, workItem.FileSystemInfo);
				}
				catch (Exception e)
				{
					workItem.ColumnProviderInfo.Enabled = false;

					string error =
						string.Format("The following unhandled exception occurred in the '{0}' column provider. It has been disabled.\n\n{1}",
						              columnProvider.GetType().Name, e.Message);
					MessageBox.Show(error, "Column Provider Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
				}
			}
		}

		private void OnItemCountChanged()
		{
			if (ItemCountChanged != null)
			{
				ItemCountChanged(this, EventArgs.Empty);		
			}
		}
	}
}